package com.boxysystems.scriptmonkey.intellij.ui;

import com.boxysystems.scriptmonkey.intellij.Constants;
import com.boxysystems.scriptmonkey.intellij.ScriptMonkeyPluginClassLoader;
import com.boxysystems.scriptmonkey.intellij.ScriptMonkeyApplicationComponent;
import com.boxysystems.scriptmonkey.intellij.ScriptMonkeyPlugin;
import com.boxysystems.scriptmonkey.intellij.action.JSFileFilter;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;

import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleBindings;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * Created by IntelliJ IDEA.
 * User: shameed
 * Date: Sep 30, 2008
 * Time: 5:10:21 PM
 */
public class ScriptCommandProcessor implements ShellCommandProcessor {

  private static final Logger logger = Logger.getLogger(ScriptCommandProcessor.class);

  private volatile ScriptEngine engine;

  private CountDownLatch engineReady = new CountDownLatch(1);

  private volatile String prompt;


  private boolean commandShell = true;
  private Application application;
  private Project project;
  private ScriptMonkeyPlugin plugin;
  private ScriptMonkeyPluginClassLoader pluginClassLoader;

  public ScriptCommandProcessor(Application application) {
    this.application = application;
    createScriptEngine(null);
  }


  public ScriptCommandProcessor(Application application, Project project, ScriptMonkeyPlugin scriptMonkeyPlugin) {
    this.application = application;
    this.project = project;
    this.plugin = scriptMonkeyPlugin;
    this.pluginClassLoader = new ScriptMonkeyPluginClassLoader(plugin);
    createScriptEngine(scriptMonkeyPlugin);
  }

  public ExecutorService processScriptFile(final File scriptFile, final ScriptProcessorCallback callback) {
    ExecutorService executor = Executors.newFixedThreadPool(1);
    executor.execute(new Runnable() {
      public void run() {
        evaluateScriptFile(scriptFile, callback);
      }
    });
    executor.shutdown();
    return executor;
  }

  public void processScriptFileSynchronously(final File scriptFile, final ScriptProcessorCallback callback) {
    ExecutorService executor = processScriptFile(scriptFile, callback);
    try {
      executor.awaitTermination(1, TimeUnit.MINUTES);
    } catch (InterruptedException e) {
      logger.error("Error while processing script file synchronously", e);
    }
  }

  private void evaluateScriptFile(File scriptFile, ScriptProcessorCallback callback) {
    try {
      initScriptingEngineAndRunGlobalScripts();
      if (scriptFile != null) {
        logger.info("Evaluating script file '" + scriptFile + "' ...");
        engine.eval(new FileReader(scriptFile));
      }
      callback.success();
    } catch (Throwable e) {
      callback.failure(e);
    }
  }

  public ScriptRunningTask processScript(final String scriptContent, final ScriptProcessorCallback callback) {
    if (project != null) {
      ScriptRunningTask task = new ScriptRunningTask("Running script...", scriptContent, callback);
      task.queue();
      return task;
    }
    return null;
  }

  private void initScriptingEngineAndRunGlobalScripts() {
    initScriptEngine();
    runGlobalScripts();
  }

  public void processCommandLine() {
    new Thread(new Runnable() {
      public void run() {
        initScriptingEngineAndRunGlobalScripts();
        engineReady.countDown();
      }
    }).start();
  }

  public String getPrompt() {
    return prompt;
  }

  public boolean isCommandShell() {
    return commandShell;
  }

  public void setCommandShell(boolean commandShell) {
    this.commandShell = commandShell;
  }

  public String executeCommand(String cmd) {
    String res;
    try {
      engineReady.await();
      Object tmp = engine.eval(cmd);
      res = (tmp == null) ? null : tmp.toString();
    } catch (InterruptedException ie) {
      res = ie.getMessage();
    } catch (ScriptException se) {
      res = se.getMessage();
    }
    return res;
  }

  private void createScriptEngine(ScriptMonkeyPlugin scriptMonkeyPlugin) {
    ScriptEngineManager manager;
    if (scriptMonkeyPlugin != null && pluginClassLoader != null) {
      ScriptMonkeyPluginClassLoader augmentedClassLoader = pluginClassLoader.getAugmentedClassLoader();
      if (augmentedClassLoader != null) {
        Thread.currentThread().setContextClassLoader(augmentedClassLoader);
      }
      manager = createScriptEngineManager();
    } else {
      manager = createScriptEngineManager();
    }
    String language = "JavaScript";
    engine = manager.getEngineByName(language);
    if (engine == null) {
      throw new RuntimeException("cannot load " + language + " engine");
    }
    String extension = engine.getFactory().getExtensions().get(0);
    prompt = extension + ">";
    engine.setBindings(createGlobalBindings(), ScriptContext.ENGINE_SCOPE);
  }

  private ScriptEngineManager createScriptEngineManager() {
    return new ScriptEngineManager();
  }

  private void runGlobalScripts() {
    try {
      ScriptMonkeyApplicationComponent applicationComponent = ApplicationManager.getApplication().getComponent(ScriptMonkeyApplicationComponent.class);
      File jsFolder = new File(applicationComponent.getSettings().getHomeFolder(), Constants.JS_FOLDER_NAME);
      File globalScripts = new File(jsFolder, "global");

      if (globalScripts.exists()) {
        File[] jsFiles = globalScripts.listFiles(new JSFileFilter());
        for (File jsFile : jsFiles) {
          try {
            logger.info("Evaluating script '" + jsFile + "' ...");
            engine.eval(new FileReader(jsFile));
            logger.info("Script successfully processed !");
          } catch (ScriptException e) {
            logger.error("Error executing script '" + jsFile + "'", e);
          } catch (FileNotFoundException e) {
            logger.error("Unable to find script file '" + jsFile + "' !");
          }
        }
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  private Bindings createGlobalBindings() {
    Map<String, Object> map =
      Collections.synchronizedMap(new HashMap<String, Object>());
    return new SimpleBindings(map);
  }

  private void initScriptEngine() {
    addGlobalVariable("engine", engine);
    addGlobalVariable("application", application);
    addGlobalVariable("project", project);
    addGlobalVariable("plugin", plugin);
  }

  public void addGlobalVariable(String name, Object globalObject) {
    if (name != null && globalObject != null) {
      engine.put(name, globalObject);
    }
  }

  public class ScriptRunningTask extends Task.Backgroundable {
    private ScriptProcessorCallback callback;
    private String scriptContent;
    private ExecutorService executor;

    public ScriptRunningTask(@NotNull String title, String scriptContent, ScriptProcessorCallback callback) {
      super(project, title, false);
      this.scriptContent = scriptContent;
      this.callback = callback;
      this.setCancelText("Stop running scripts");
    }

    public void cancel() {
      executor.shutdownNow();
    }

    public boolean isRunning() {
      return !executor.isTerminated();
    }


    public void run(ProgressIndicator indicator) {
      executor = Executors.newFixedThreadPool(1);
      executor.execute(new Runnable() {
        public void run() {
          initScriptingEngineAndRunGlobalScripts();
          try {

            if (scriptContent != null) {
              logger.info("Evaluating script ...");
              engine.eval(scriptContent);
            }
            callback.success();
          } catch (Throwable e) {
            callback.failure(e);
          }

        }
      });
    }
  }
}
